Retail data captures everyday transactions—what customers buy, when they buy, and how much they spend. It’s rich with patterns that help businesses understand customer behavior, demand trends, and revenue drivers.
We’re simulating a retail analytics pipeline where:
We’ll transform raw transactional data into:
Your dataset contains:
Goal: Turn raw, messy data into a clean and usable dataset for analysis & ML.
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
df = pd.read_csv('Online Retail.csv', encoding="latin1")
df
| CustomerID | InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | |
|---|---|---|---|---|---|---|---|
| 0 | 17850.0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 |
| 1 | 17850.0 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 |
| 2 | 17850.0 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 |
| 3 | 17850.0 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 |
| 4 | 17850.0 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 541904 | 12680.0 | 581587 | 22613 | PACK OF 20 SPACEBOY NAPKINS | 12 | 2011-12-09 12:50:00 | 0.85 |
| 541905 | 12680.0 | 581587 | 22899 | CHILDREN'S APRON DOLLY GIRL | 6 | 2011-12-09 12:50:00 | 2.10 |
| 541906 | 12680.0 | 581587 | 23254 | CHILDRENS CUTLERY DOLLY GIRL | 4 | 2011-12-09 12:50:00 | 4.15 |
| 541907 | 12680.0 | 581587 | 23255 | CHILDRENS CUTLERY CIRCUS PARADE | 4 | 2011-12-09 12:50:00 | 4.15 |
| 541908 | 12680.0 | 581587 | 22138 | BAKING SET 9 PIECE RETROSPOT | 3 | 2011-12-09 12:50:00 | 4.95 |
541909 rows × 7 columns
df.shape
(541909, 7)
df.head()
| CustomerID | InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | |
|---|---|---|---|---|---|---|---|
| 0 | 17850.0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 |
| 1 | 17850.0 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 |
| 2 | 17850.0 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 |
| 3 | 17850.0 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 |
| 4 | 17850.0 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 |
df.columns
Index(['CustomerID', 'InvoiceNo', 'StockCode', 'Description', 'Quantity',
'InvoiceDate', 'UnitPrice'],
dtype='object')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 541909 entries, 0 to 541908 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 CustomerID 406829 non-null float64 1 InvoiceNo 541909 non-null object 2 StockCode 541909 non-null object 3 Description 540455 non-null object 4 Quantity 541909 non-null int64 5 InvoiceDate 541909 non-null object 6 UnitPrice 541909 non-null float64 dtypes: float64(2), int64(1), object(4) memory usage: 28.9+ MB
df.describe()
| CustomerID | Quantity | UnitPrice | |
|---|---|---|---|
| count | 406829.000000 | 541909.000000 | 541909.000000 |
| mean | 15287.690570 | 9.552250 | 4.611114 |
| std | 1713.600303 | 218.081158 | 96.759853 |
| min | 12346.000000 | -80995.000000 | -11062.060000 |
| 25% | 13953.000000 | 1.000000 | 1.250000 |
| 50% | 15152.000000 | 3.000000 | 2.080000 |
| 75% | 16791.000000 | 10.000000 | 4.130000 |
| max | 18287.000000 | 80995.000000 | 38970.000000 |
df.isnull().sum()
CustomerID 135080 InvoiceNo 0 StockCode 0 Description 1454 Quantity 0 InvoiceDate 0 UnitPrice 0 dtype: int64
The most critical column — CustomerID — has the highest missing values (135K+)
This is not just a data issue… it’s a business reality.
Think Before You Clean
At this point, instead of rushing to fix it, pause and ask:
❓ What are we trying to build?
Because your decision here will directly impact:
CustomerID: Dropped nulls since it's essential for ML & customer-level analysis
Note: You won’t always drop this — it depends on your goal. For sales analysis, you might keep it.
Description: Filled with "Unknown Product" to retain data without losing rows
df = df.dropna(subset=["CustomerID"])
df["Description"] = df["Description"].fillna("Unknown Product")
df.shape
(406829, 7)
df["InvoiceDate"] = pd.to_datetime(df["InvoiceDate"])
df["CustomerID"] = df["CustomerID"].astype(int)
# Remove negative or zero quantity (returns/cancellations)
df = df[df["Quantity"] > 0]
# Remove zero or negative prices
df = df[df["UnitPrice"] > 0]
df = df.drop_duplicates()
df.describe()
| CustomerID | Quantity | InvoiceDate | UnitPrice | |
|---|---|---|---|---|
| count | 392692.000000 | 392692.000000 | 392692 | 392692.000000 |
| mean | 15287.843865 | 13.119702 | 2011-07-10 19:13:07.771892480 | 3.125914 |
| min | 12346.000000 | 1.000000 | 2010-12-01 08:26:00 | 0.001000 |
| 25% | 13955.000000 | 2.000000 | 2011-04-07 11:12:00 | 1.250000 |
| 50% | 15150.000000 | 6.000000 | 2011-07-31 12:02:00 | 1.950000 |
| 75% | 16791.000000 | 12.000000 | 2011-10-20 12:53:00 | 3.750000 |
| max | 18287.000000 | 80995.000000 | 2011-12-09 12:50:00 | 8142.750000 |
| std | 1713.539549 | 180.492832 | NaN | 22.241836 |
We started with messy, real-world data—and turned it into something structured, reliable, and ready to work with.
Clean data isn’t exciting… but it’s where 80% of real work happens.
Everything that follows—analysis, insights, ML—depends on this foundation.
Now that the data is clean… what stories is it hiding?
We’ll move into:
Because clean data is just the beginning—insight is where things get interesting.